﻿using System;
using System.Collections.Generic;
using System.Data;
using System.Data.Common;
using Jayden.Dll.Core.Persistence.Attributes;
using Jayden.Dll.Core.Persistence.Enums;
using Jayden.Dll.Core.Persistence.Models;
using Jayden.Dll.Core.Persistence.Queries;
using Jayden.Dll.Core.Persistence.Tools;

namespace Jayden.Dll.Core.Persistence.Data
{
	public abstract class DbDataAccess<T> : DataAccess<T> where T:Object
	{
		private Configuration m_Configuration;
		private UnitOfWork m_UnitOfWork;
		private Entity m_Entity;
		protected DbConnectionProxy m_Connection = null;

		public DbDataAccess(Configuration config, UnitOfWork uow, Entity entity)
		{
			m_Configuration = config;
			m_Entity = entity;
			m_UnitOfWork = uow;
		}

		public string Ldsn { get { return m_Configuration.GetValue("@ldsn"); } }
		public UnitOfWork UnitOfWork { get { return m_UnitOfWork; } }
		public Entity Entity { get { return m_Entity; } }

		protected virtual DbConnectionProxy Connection
		{
			get
			{
				if (m_Connection == null)
					m_Connection = UnitOfWork.DbConnectionManager.GetConnection(Ldsn);
				return m_Connection;
			}
		}

		protected abstract void AssignLastGeneratedObjectId(T obj);
		protected abstract string CreateCommandText { get; }
		protected abstract string ReadCommandText { get; }
		protected abstract string UpdateCommandText { get; }
		protected abstract string DeleteCommandText { get; }
		protected abstract string CreateTableCommandText { get; }
		protected abstract string DropTableCommandText { get; }

		protected DbCommandProxy CreateCommand(string commandText)
		{
			DbCommandProxy command = Connection.CreateCommand(commandText);
			return command;
		}



		protected virtual DbType GetType(object value)
		{
			return SQLHelper.GetDefaultDbType(value.GetType());
		}
		protected virtual void AssignParameter(DbParameter parameter, object value)
		{
			parameter.Value = value;
		}
		protected virtual object ReadParameter(string parameterName, Type type, DbDataReader reader)
		{
			return reader[parameterName];
		}

		protected DbParameter CreateParameter(DbCommandProxy command, string parameterName, object value)
		{
			DbParameter parameter = command.Command.CreateParameter();
			parameter.ParameterName = parameterName;
			if (value == null)
			{
				parameter.DbType = System.Data.DbType.Binary;
				parameter.Value = DBNull.Value;
			}
			else
			{
				parameter.DbType = GetType(value);
				AssignParameter(parameter, value);
			}
			command.Command.Parameters.Add(parameter);
			return parameter;
		}
		protected DbParameter CreateParameter(DbCommandProxy command, Field field, object value)
		{
			DbParameter parameter = command.Command.CreateParameter();
			parameter.ParameterName = field.FieldName;
			parameter.DbType = field.DbType;
			if (value == null)
				parameter.Value = DBNull.Value;
			else
				AssignParameter(parameter, value);
			command.Command.Parameters.Add(parameter);
			return parameter;
		}

		protected void CheckObjectEntity(object obj)
		{
			if (obj == null)
				throw new ArgumentNullException("obj");
			if (!(obj is T))
				throw new Exception(string.Format("Invalid object type '{0}' for  DbDataAccess<{1}>.", obj.GetType().FullName, typeof(T).FullName));
			T t = (T)obj;
			CheckObjectEntity(t.ObjectId);
		}
		protected void CheckObjectEntity(ObjectId oid)
		{
			if (oid == null)
				throw new ArgumentNullException("oid");
			if (oid.Lenght != Entity.PrimaryKey.Length)
				throw new Exception(string.Format("Invalid ObjectType length {0} for DbDataAccess<{1}>, excepting {2}", oid.Lenght, typeof(T).FullName, Entity.PrimaryKey.Length));
			if (!oid.Type.Equals(Entity.ObjectType))
				throw new Exception(string.Format("Invalid ObjectType '{0}' for DbDataAccess<{1}>, excepting '{2}'", oid.Type, typeof(T).FullName, Entity.ObjectType));
		}

		public void Create(T obj) { CreateInternal(obj); }
		public void Create(Object obj) { CreateInternal((T)obj); }
		protected void CreateInternal(T obj)
		{
			CheckObjectEntity(obj);
			string createCommandText = CreateCommandText;
			using (DbCommandProxy createCommand = CreateCommand(createCommandText))
			{
				foreach (Property property in Entity.Properties)
					if(!property.Field.AutoIncrement)
						CreateParameter(createCommand, property.Field, property.GetValue(obj));
				int result = createCommand.Command.ExecuteNonQuery();
				AssignLastGeneratedObjectId(obj);
			}
		}


		public Configuration Configuration { get { return m_Configuration; } }
		public T Read(ObjectId id) { return ReadInternal(id); }
		Object DataAccess.Read(ObjectId id) { return ReadInternal(id); }
		public T ReadInternal(ObjectId id)
		{
			CheckObjectEntity(id);
			Index pk = Entity.PrimaryKey;
			string readCommandText = ReadCommandText;
			using (DbCommandProxy readCommand = CreateCommand(readCommandText))
			{
				for (int c = 0; c < pk.Length; c++)
					CreateParameter(readCommand, pk[c].Field, id[c]);
				using (DbDataReader reader = readCommand.Command.ExecuteReader())
				{
					return CreateFromReader(reader);
				}
			}
		}

		public void Update(T obj) { UpdateInternal(obj); }
		public void Update(Object obj) { UpdateInternal((T)obj); }
		protected void UpdateInternal(T obj)
		{
			CheckObjectEntity(obj);
			string updateCommandText = UpdateCommandText;
			using (DbCommandProxy updateCommand = CreateCommand(updateCommandText))
			{
				foreach (Property property in Entity.Properties)
					CreateParameter(updateCommand, property.Field, property.GetValue(obj));
				int result = updateCommand.Command.ExecuteNonQuery();
			}
		}

		public void Delete(ObjectId id)
		{
			CheckObjectEntity(id);
			Index pk = Entity.PrimaryKey;
			string deleteCommandText = DeleteCommandText;
			using (DbCommandProxy deleteCommand = CreateCommand(deleteCommandText))
			{
				for (int c = 0; c < pk.Length; c++)
					CreateParameter(deleteCommand, pk[c].Field, id[c]);
				int result = deleteCommand.Command.ExecuteNonQuery();
			}
		}

		public void CreateTable()
		{
			string createTableCommandText = CreateTableCommandText;
			using (DbCommandProxy createTableCommand = CreateCommand(createTableCommandText))
			{
				int result = createTableCommand.Command.ExecuteNonQuery();
			}
		}
		public void DropTable()
		{
			string dropTableCommandText = DropTableCommandText;
			using (DbCommandProxy dropTableCommand = CreateCommand(dropTableCommandText))
			{
				int result = dropTableCommand.Command.ExecuteNonQuery();
			}
		}


		protected object GetPropertyValueFromDataReader(Property property, System.Data.Common.DbDataReader reader)
		{
			string fieldName = property.Field.FieldName;
			object value = ReadParameter(fieldName, property.PropertyInfo.PropertyType, reader);
			if (value == DBNull.Value)
				value = null;
			if (property.PropertyInfo.PropertyType.IsEnum)
				value = Enum.Parse(property.PropertyInfo.PropertyType, value.ToString());
			return value;
		}

		protected void AssignObjectIdFromReader(ObjectId oid, System.Data.Common.DbDataReader reader)
		{
			for (int c = 0; c < Entity.PrimaryKey.Length; c++)
			{
				Property p = Entity.PrimaryKey[c];
				oid[c] = GetPropertyValueFromDataReader(p, reader);
			}
		}

		protected virtual T CreateFromReader(System.Data.Common.DbDataReader reader)
		{
			T obj = default(T);
			if (reader.Read())
			{
				ObjectId oid = UnitOfWork.NewObjectId(Entity);
				AssignObjectIdFromReader(oid, reader);
				lock (UnitOfWork.RegistrationLock)
				{
					obj = UnitOfWork.GetRegistered<T>(oid);
					if (obj != null)
						return obj;
					obj = UnitOfWork.NewObject<T>(Entity, oid);
					foreach (Property p in Entity.Properties)
						p.SetValue(obj, GetPropertyValueFromDataReader(p, reader));
					obj.State = ObjectState.Clean;
					UnitOfWork.Register(obj);
				}
			}
			return obj;
		}


		protected virtual IEnumerable<T> EnumerateFromReader(System.Data.Common.DbDataReader reader)
		{
			T entity = CreateFromReader(reader);
			while (entity != null)
			{
				yield return entity;
				entity = CreateFromReader(reader);
			}
		}


		public virtual string GetQueryCommandText(Query query) { return query.ToString(); }
		public DataTable Read(Query query, Dictionary<string, object> parameters = null)
		{
			string queryCommandText = GetQueryCommandText(query);
			using (DbCommandProxy queryCommand = CreateCommand(queryCommandText))
			{
				if(parameters != null && parameters.Count > 0)
					foreach (KeyValuePair<string, object> parameter in parameters)
						CreateParameter(queryCommand, parameter.Key, parameter.Value);
				using (DbDataReader reader = queryCommand.Command.ExecuteReader())
				{
					DataTable table = new DataTable();
					table.Load(reader);
					return table;
				}
			}
		}

		public void Dispose()
		{
			if (m_Connection != null)
				m_Connection.Dispose();
		}

		public void BeginTransaction()
		{
			if (m_Connection != null && !m_Connection.InTransation)
				m_Connection.BeginTransaction();
		}

		public void CommitTransaction()
		{
			if (m_Connection != null && m_Connection.InTransation)
				m_Connection.CommitTransaction();
		}

		public void RollbackTransaction()
		{
			if (m_Connection != null && m_Connection.InTransation)
				m_Connection.RollbackTransaction();
		}
	}
}
